package com.fowo.api.syncLingxingApi.service;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fowo.api.datainterface.constants.DataTypeKind;
import com.fowo.api.datainterface.constants.RequestMethodKind;
import com.fowo.api.datainterface.entity.DatainterfaceAuthority;
import com.fowo.api.datainterface.service.DatainterfaceAuthorityService;
import com.fowo.api.datainterface.util.Common;
import com.fowo.api.datainterface.util.RestUtilLingXing;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;

@Service
@Slf4j
@DS("ds24")
public abstract class SyncLxApiBaseService <NewT, OldT> {
    protected final int reqMaxLen;
    protected static int RETRY_TIMES = 10;

    @Resource
    protected DatainterfaceAuthorityService datainterfaceAuthorityService;
    @Resource
    protected RestUtilLingXing restUtilLingXing;
    protected String lxApiToken;
    protected String lxApiAppId;
    protected String lxApiAppSecret;
    protected String lxApiTokenUrl;
    protected String lxApiUrl;
    protected String data_version;

    protected long curOffset = 0;
    protected long totalNum = 1;

    private final String lxApiUrlKey;

    protected enum LOG_LEVEL {
        DEBUG, INFO, WARNING, ERROR
    }

    /****************/
    public void mainLoop(JSONObject requestBody) {
        if (!initData()) {
            return;
        }

        beforeLoop();
        long startTimeMillis = System.currentTimeMillis();

        while (loopCond()) {
            long blockStartTimeMills = System.currentTimeMillis();
            long dataSize = 0;
            long reqDataSize = 0;
            JSONObject newResults = pullLingxingData(curOffset, this.reqMaxLen, requestBody);
//            log.info("Request api: " + this.lxApiUrl);
            if (newResults != null && Objects.equals(newResults.getString("code"), "0")) {
                dataSize = newResults.getInteger("total");

                JSONArray reqBody = newResults.getJSONArray("data");
                reqDataSize = reqBody.size();
//                log.info("request data size is " + String.valueOf(reqDataSize));
                this.logInfo("request data size is " + String.valueOf(reqDataSize));

                if (reqDataSize <= 0) {
                    this.logWarnning("Finished. Request data size is 0." + newResults.toString());
                    break;
                }

                List<NewT> newObjects = jsonDumpsResult(reqBody);
//                log.info("new data size is " + String.valueOf(newObjects.size()) + ".");
                this.logInfo("new data size is " + String.valueOf(newObjects.size()) + ".");

                List<OldT> oldObjects = getOldData(newObjects);
                this.logInfo("old data size is " + (oldObjects == null? "0": String.valueOf(oldObjects.size())) + ".");

                List<NewT> cmpResults = compareNewAndOldData(newObjects, oldObjects);
                this.logInfo("after compare size is " + String.valueOf(cmpResults.size()) + ".");

                saveData2TmpTb(cmpResults);

            } else {
                this.logError("Request api failed.");
                break;
            }
            //
            this.continueDo(dataSize, curOffset, reqDataSize);
            this.logInfo("Process speed：" + String.valueOf(curOffset) + "/" + String.valueOf(totalNum) + ". total time is "
                    + String.valueOf((System.currentTimeMillis() - blockStartTimeMills) / 1000.0) + "sec / "
                    + String.valueOf((System.currentTimeMillis() - startTimeMillis) / 1000.0) + " sec.");
        }
        saveTempToFormal();

        afterLoop();

        this.logInfo("Total data size is " + String.valueOf(totalNum) + ". Total time is "
                + String.valueOf((System.currentTimeMillis() - startTimeMillis) / 1000.0) + " sec.");
    }

    /*
    params:
        lxApiUrlKey: 接口中心，数据库中配置的key值
     */
    public SyncLxApiBaseService(String lxApiUrlKey, int maxLen) {
        this.lxApiUrlKey = lxApiUrlKey;
        this.reqMaxLen = maxLen;

    }

    protected void beforeLoop() {}
    protected void afterLoop() {}

    protected boolean initData() {
        DatainterfaceAuthority entityAuthority = datainterfaceAuthorityService.getOne(
                Wrappers.<DatainterfaceAuthority>lambdaQuery()
                        .eq(DatainterfaceAuthority::getAppCode, DataTypeKind.Lingxing),
                false);

        String appId = Common.getAuthoritySimpleFactory(entityAuthority, "appId");
        String appSecret = Common.getAuthoritySimpleFactory(entityAuthority, "appSecret");
        String tokenUrl = Common.getAuthoritySimpleFactory(entityAuthority, "tokenUrl");
        String apiUrl = Common.getAuthoritySimpleFactory(entityAuthority, this.lxApiUrlKey);

        Assert.isTrue(ObjectUtil.isAllNotEmpty(appId, appSecret), "请配置授权格式,例: {\"appId\":\"\",\"appSecret\":\"\"}");

        this.lxApiAppId = appId;
        this.lxApiAppSecret = appSecret;
        this.lxApiTokenUrl = tokenUrl;
        this.lxApiUrl = apiUrl;
        this.lxApiToken = getToken();
        this.data_version = String.valueOf(System.currentTimeMillis());

        this.curOffset = 0;
        this.totalNum = 1;

        return this.lxApiToken != null && !this.lxApiToken.isEmpty();
    }

    protected String getToken() {
        final Map<String, Object> REQUEST_PARAMS = Map.of(
                "appId", this.lxApiAppId, "appSecret", this.lxApiAppSecret);

        for (int i = 0; i != RETRY_TIMES; ++i) {
            Object lingXingRequest = restUtilLingXing.lingXingRequest(
                    "1", RequestMethodKind.Post.getValue(), REQUEST_PARAMS,
                    this.lxApiTokenUrl, null, null);
            JSONObject jsonObject = null;
            try {
                jsonObject = (JSONObject) lingXingRequest;
            }
            catch (ClassCastException ex) {
                logOut("连接" + this.lxApiToken + "异常", LOG_LEVEL.WARNING);
                continue;
            }


            // 应该还有个返回的错误码不等于0
            if (!"200".equals(jsonObject.get("code"))) {
                logWarnning("尝试" + String.valueOf(i + 1) + "/" + String.valueOf(RETRY_TIMES) +
                        "，获取token失败: " + jsonObject.toJSONString());
                continue;
            } else {
                //取得access-token
                return ((JSONObject) jsonObject.get("data")).get("access_token").toString();
            }
        }
        // MARK: 输出日志 获取日志失败
        logError("重试" + String.valueOf(RETRY_TIMES) + "次后，获取token失败！");
        return null;
    }

    protected JSONObject tryRequestLingxingApi(Map<String, Object> requestBody) {
        JSONObject resultJson = null;
        Map<String, Object> headers = new HashMap<>();
        for (int i = 0; i < RETRY_TIMES; ++i) {
            Map<String, Object> requestParams = new HashMap<>();
            requestParams.put("access_token", this.lxApiToken);
            requestParams.put("app_key", this.lxApiAppId);
            requestParams.put("timestamp", System.currentTimeMillis() / 1000);

            resultJson = (JSONObject) restUtilLingXing.lingXingRequestBatch(
                    "2",
                    RequestMethodKind.Post.getValue(),
                    requestParams,
                    this.lxApiUrl,
                    headers,
                    requestBody
            );

            Integer code = resultJson.getInteger("code");
            if (code == 0) {
                break;
            } else if (code == 500) {
                logError("请求数据失败." + resultJson.toString());
                break;
            } else if (i == RETRY_TIMES - 1) {
                logWarnning("拉取数据重试多次失败. Data=" + requestBody.toString() + "。Url=" + this.lxApiUrl);
            } else {
                logWarnning("请求失败5秒后重试。" + resultJson.toJSONString());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    logError("系统程序异常. " + e.toString());
                }
                if (code == 3001008 || code == 2001005) {
                    this.lxApiToken = getToken();
                    // MARK: log.output
                    logWarnning(resultJson.toJSONString());
                }
            }
        }

        return resultJson;
    }

    protected abstract JSONObject pullLingxingData(long offset, long length, JSONObject params);

    protected List<NewT> compareNewAndOldData(List<NewT> newData, List<OldT> oldData) {
        List<NewT> results = new ArrayList<>();

        while (!newData.isEmpty()) {
            NewT newOne = newData.get(0);
            newData.remove(newOne);

            OldT oldOne = null;
            for (OldT item: oldData) {
                if (cmpNewAndOldEqualCond(newOne, item)) {
                    oldOne = item;
                    oldData.remove(item);
                    break;
                }
            }

            if (oldOne == null) {
                newOne = cmpNewAndOldInsertProc(newOne);
            }
            else if (this.cmpNewAndOldUpdateCond(newOne, oldOne)) {
                newOne = this.cmpNewAndOldUpdateProc(newOne, oldOne);
            }
            else {
                continue;
            }
            results.add(newOne);
        }
        return results;
    }
    /*

     */
    protected boolean loopCond() {
        return this.totalNum > this.curOffset;
    }
    protected void continueDo(long totalNum, long offset, long reqTotalNum) {
        this.curOffset += reqTotalNum;
        if (offset < totalNum) {
            this.totalNum = totalNum;
        }
    }
    /*
        判断新数据是否在旧数据中的关键字条件，目的是为了拿旧数据
     */
    protected abstract boolean cmpNewAndOldEqualCond(NewT newOne, OldT oldOne);
    /*
        插入操作的数据处理过程，条件是没有旧数据，只有新数据
     */
    protected abstract NewT cmpNewAndOldInsertProc(NewT newOne);
    /*
        更新操作的数据处理过程，不涉及到数据库
     */
    protected abstract NewT cmpNewAndOldUpdateProc(NewT newOne, OldT oldOne);
    /*
        判断是否需要更新的条件
     */
    protected abstract boolean cmpNewAndOldUpdateCond(NewT newOne, OldT oldOne);
    /*
        保存到正式表，即OldT数据库表中
     */
    protected abstract void saveTempToFormal();
    /*
        解析接口返回的json为对象
     */
    protected abstract List<NewT> jsonDumpsResult(JSONArray jsonArray);
    /*
        查询旧数据，以供比较
     */
    protected abstract List<OldT> getOldData(List<NewT> newObjects);
    protected abstract void saveData2TmpTb(List<NewT> results);

    protected void logInfo(String text) {
        this.logOut(text, LOG_LEVEL.INFO);
    }

    protected void logWarnning(String text) {
        this.logOut(text, LOG_LEVEL.WARNING);
    }

    protected void logError(String text) {
        this.logOut(text, LOG_LEVEL.ERROR);
    }

    protected void logDebug(String text) {
        this.logOut(text, LOG_LEVEL.DEBUG);
    }

    protected void logOut(String text, LOG_LEVEL level) {
        text = this.lxApiUrlKey + " >> " + text;
        switch (level) {
            case DEBUG:
                log.debug(text);
                break;

            case WARNING:
                log.warn(text);
                break;

            case ERROR:
                log.error(text);
                break;

            default:
                log.info(text);
                break;
        }
    }
}
